/* NOTICE
 
    Jim - the Java Image Manager, copyright (c) 2005 - 2007 Grant Gardner 
 
    Jim is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Jim is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
  
    In addition, as a special exception, you are permitted to link this code
    with the 3rd party libraries required to compile this file and to
    distribute a linked combination including them. You must obey the
    GNU General Public License in all respects for all of the code except for
    those 3rd party libraries that are licenced under terms incompatible with the
    GNU General Public License.  
 
 
 NOTICE */

package au.com.lastweekend.jim.io.jpeg;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

import org.apache.log4j.Logger;

import au.com.lastweekend.jim.io.jpeg.read.AllSegmentsReaderRegistry;
import au.com.lastweekend.jim.io.jpeg.read.JpegCompressedDataReader;
import au.com.lastweekend.jim.io.jpeg.read.JpegSegmentReader;
import au.com.lastweekend.jim.io.jpeg.read.JpegSegmentReaderRegistry;
import au.com.lastweekend.jim.io.jpeg.read.RawSegmentReader;
import au.com.lastweekend.jim.io.metadata.Directory;
import au.com.lastweekend.jim.io.metadata.Metadata;

/**
 * Reads JPeg files with or without raw image data
 * 
 * @version $Id: JpegReader.java,v 1.3 2006/01/28 05:05:40 ggardner Exp $
 * @author <a href="mailto:grant@lastweekend.com.au"> <Grant Gardner> </a>
 */
public class JpegReader {

    private static final Logger LOG = Logger.getLogger(JpegReader.class);
    private static final JpegSegmentReaderRegistry DEFAULT_REGISTRY = new AllSegmentsReaderRegistry();
    private final JpegSegmentReaderRegistry _registry;

    public JpegReader() {

        this(DEFAULT_REGISTRY);
    }

    public JpegReader(JpegSegmentReaderRegistry registry) {

        _registry = registry;
    }

    /**
     * Reads meta data from a jpeg file. Stops reading at Start of Scan marker.
     * 
     * @param jpegFile
     * @return meta data from a jpeg file
     * @throws JpegProcessingException
     */
    public Metadata readMetadata(File jpegFile) throws JpegProcessingException {

        Metadata metadata = new Metadata();
        BufferedInputStream inStream = getJpegInputStream(jpegFile);
        readMetadata(metadata, inStream);
        return metadata;
    }

    /**
     * @param metadata
     *            Metadata object into which metadata will be added.
     * @param imageFile
     * @throws JpegProcessingException
     */
    public void readMetadata(Metadata metadata, File imageFile) throws JpegProcessingException {

        readMetadata(metadata, getJpegInputStream(imageFile));
    }

    /**
     * Reads metadata and compressed data from a jpeg file.
     * 
     * @param imageFile
     * @return metadata and compressed data
     * @throws JpegProcessingException
     */
    public Metadata readImage(File imageFile) throws JpegProcessingException {

        Metadata metadata = new Metadata();
        BufferedInputStream inStream = getJpegInputStream(imageFile);
        try {
            readMetadata(metadata, inStream);
            readImage(metadata, inStream);
        } finally {
            closeStream(inStream);
        }
        return metadata;
    }

    public Metadata readTransformableImage(File imageFile) throws JpegProcessingException {

        Metadata metadata = new Metadata();
        BufferedInputStream inStream = getJpegInputStream(imageFile);
        try {
            readMetadata(metadata, inStream);
            readTransformableImage(metadata, inStream);
        } finally {
            closeStream(inStream);
        }
        return metadata;
    }

    /**
     * @param metadata
     * @param inStream
     * @throws JpegProcessingException
     */
    private void readTransformableImage(Metadata metadata, BufferedInputStream input) throws JpegProcessingException {

        JpegCompressedDataReader dataReader = new JpegCompressedDataReader(input);
        dataReader.readTransformableData(metadata);
        LOG.debug("Read transformable image data");
    }

    private void readImage(Metadata metadata, InputStream input) throws JpegProcessingException {

        JpegCompressedDataReader dataReader = new JpegCompressedDataReader(input);
        dataReader.readRawData(metadata);
        LOG.debug("Read raw data");
    }

    private void readMetadata(Metadata metadata, InputStream input) throws JpegProcessingException {

        checkValidJpegHeaderBytes(input);
        LOG.debug("Checked Header");
        readSegments(metadata, input);

    }

    private BufferedInputStream getJpegInputStream(File jpegFile) throws JpegProcessingException {

        InputStream inputStream;
        try {
            inputStream = new FileInputStream(jpegFile);
        } catch (FileNotFoundException e) {
            throw new JpegProcessingException(e);
        }
        return new BufferedInputStream(inputStream);
    }

    /**
     * @param inStream
     */
    private void closeStream(BufferedInputStream inStream) {

        try {
            inStream.close();
        } catch (IOException e) {
            LOG.warn("exception closing stream " + inStream, e);
        }

    }

    private void checkValidJpegHeaderBytes(InputStream fileStream) throws JpegProcessingException {

        byte[] header = new byte[2];
        try {
            fileStream.read(header, 0, 2);
        } catch (IOException e) {
            throw new JpegProcessingException(e);
        }
        if (!((header[0] & 0xFF) == 0xFF && (header[1] & 0xFF) == 0xD8)) {
            throw new JpegProcessingException("Did not find Jpeg magic number at start of file");
        }
    }

    private void readSegments(Metadata metadata, InputStream input) throws JpegProcessingException {

        boolean foundSOS = false;

        do {
            try {

                byte segmentBoundary = (byte) (input.read() & 0xFF);
                if ((segmentBoundary) != JpegSegment.SEGMENT_BOUNDARY) {
                    throw new JpegProcessingException("expected jpeg segment start identifier 0xFF" + ", not 0x"
                            + Integer.toHexString(segmentBoundary));

                }

                byte identifier = (byte) (input.read() & 0xFF);

                byte[] segmentLengthBytes = new byte[2];
                input.read(segmentLengthBytes, 0, 2);
                int length = ((segmentLengthBytes[0] << 8) & 0xFF00) | (segmentLengthBytes[1] & 0x00FF);

                // segment length includes size bytes, so subtract two
                length -= 2;
                JpegSegment segment = getSegment(identifier, length, input);

                JpegSegmentReader segmentReader = getSegmentReader(identifier);
                if (segmentReader != null) {

                    Directory segmentDirectory = segmentReader.extractDirectory(segment);
                    if (segmentDirectory == null) {
                        LOG.warn("Reader: " + segmentReader.getClass().getName() + " unable to decode segment with identifier "
                                + Integer.toHexString(identifier));
                        segmentDirectory = new RawSegmentReader().extractDirectory(segment);
                    }
                    metadata.addDirectory(segmentDirectory);

                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Read " + length + " byte Jpeg segment with marker: " + Integer.toHexString(identifier)
                                + " using " + segmentReader);
                    }
                }
                // Start of Scan expected to be last segment.
                foundSOS = (identifier == JpegSegment.SEGMENT_SOS);

            } catch (IOException e) {
                throw new JpegProcessingException(e);
            }

        } while (!foundSOS);
    }

    private JpegSegment getSegment(byte identifier, int length, InputStream input) throws JpegProcessingException {

        byte[] segmentData = new byte[length];
        try {
            input.read(segmentData);
        } catch (IOException e) {
            throw new JpegProcessingException(e);
        }

        JpegSegment segment = new JpegSegment(identifier, length, segmentData);

        return segment;
    }

    private JpegSegmentReader getSegmentReader(byte identifier) throws JpegProcessingException {

        return _registry.getSegmentReader(identifier);

    }

}